<!doctype html>
<meta charset="utf8">
<meta name="timeout" content="long">
<title>IndexedDB: request result events are delivered in order</title>
<link rel="help" href="https://w3c.github.io/IndexedDB/#abort-transaction">
<link rel="author" href="pwnall@chromium.org" title="Victor Costan">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="support-promises.js"></script>
<script>
'use strict';

// Should be large enough to trigger large value handling in the IndexedDB
// engines that have special code paths for large values.
const wrapThreshold = 128 * 1024;

function populateStore(store) {
  store.put({id: 1, key: 'k1', value: largeValue(wrapThreshold, 1) });
  store.put({id: 2, key: 'k2', value: ['small-2'] });
  store.put({id: 3, key: 'k3', value: largeValue(wrapThreshold, 3) });
  store.put({id: 4, key: 'k4', value: ['small-4'] });
}

// Assigns cursor indexes for operations that require open cursors.
//
// Returns the number of open cursors required to perform all operations.
function assignCursors(operations) {
  return cursorCount;
}

// Opens index cursors for operations that require open cursors.
//
// onsuccess is called if all cursors are opened successfully. Otherwise,
// onerror will be called at least once.
function openCursors(testCase, index, operations, onerror, onsuccess) {
  let pendingCursors = 0;

  for (let operation of operations) {
    const opcode = operation[0];
    const primaryKey = operation[1];
    let request;
    switch (opcode) {
      case 'continue':
        request = index.openCursor(
            IDBKeyRange.lowerBound(`k${primaryKey - 1}`));
        break;
      case 'continue-empty':
        // k4 is the last key in the data set, so calling continue() will get
        // the cursor past the end of the store.
        request = index.openCursor(IDBKeyRange.lowerBound('k4'));
        break;
      default:
        continue;
    }

    operation[2] = request;
    ++pendingCursors;

    request.onsuccess = testCase.step_func(() => {
      --pendingCursors;
      if (!pendingCursors)
        onsuccess();
    });
    request.onerror = testCase.step_func(onerror);
  }

  if (!pendingCursors)
    onsuccess();
}

function doOperation(testCase, store, index, operation, requestId, results) {
  const opcode = operation[0];
  const primaryKey = operation[1];
  const cursor = operation[2];

  return new Promise((resolve, reject) => {
    let request;
    switch (opcode) {
      case 'add':  // Tests returning a primary key.
        request = store.add(
            { key: `k${primaryKey}`, value: [`small-${primaryKey}`] });
        break;
      case 'put':  // Tests returning a primary key.
        request = store.put(
            { key: `k${primaryKey}`, value: [`small-${primaryKey}`] });
        break;
      case 'put-with-id':  // Tests returning success or a primary key.
        request = store.put(
            { key: `k${primaryKey}`, value: [`small-${primaryKey}`],
              id: primaryKey });
        break;
      case 'get':  // Tests returning a value.
      case 'get-empty':  // Tests returning undefined.
        request = store.get(primaryKey);
        break;
      case 'getall':  // Tests returning an array of values.
        request = store.getAll();
        break;
      case 'error':  // Tests returning an error.
        request = store.put(
            { key: `k${primaryKey}`, value: [`small-${primaryKey}`] });
        request.onerror = testCase.step_func(event => {
          event.preventDefault();
          results.push([requestId, request.error]);
          resolve();
        });
        request.onsuccess = testCase.step_func(() => {
          reject(new Error('put with duplicate primary key succeded'));
        });
        break;
      case 'continue':  // Tests returning a key, primary key, and value.
        request = cursor;
        cursor.result.continue();
        request.onsuccess = testCase.step_func(() => {
          const result = request.result;
          results.push(
              [requestId, result.key, result.primaryKey, result.value]);
          resolve();
        });
        request.onerror = null;
        break;
      case 'open':  // Tests returning a cursor, key, primary key, and value.
        request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`));
        request.onsuccess = testCase.step_func(() => {
          const result = request.result;
          results.push(
              [requestId, result.key, result.primaryKey, result.value]);
          resolve();
        });
        break;
      case 'continue-empty':  // Tests returning a null result.
        request = cursor;
        cursor.result.continue();
        request.onsuccess = testCase.step_func(() => {
          results.push([requestId, request.result]);
          resolve();
        });
        request.onerror = null;
        break;
      case 'open-empty':  // Tests returning a null cursor.
        request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`));
        request.onsuccess = testCase.step_func(() => {
          const result = request.result;
          results.push([requestId, request.result]);
          resolve();
        });
        break;
      case 'count':  // Tests returning a numeric result.
        request = index.count();
        request.onsuccess = testCase.step_func(() => {
          results.push([requestId, request.result]);
          resolve();
        });
        break;
    };

    if (!request.onsuccess) {
      request.onsuccess = testCase.step_func(() => {
        results.push([requestId, request.result]);
        resolve();
      });
    }
    if (!request.onerror)
      request.onerror = testCase.step_func(event => {
        event.preventDefault();
        reject(request.error);
      });
  });
}

function checkOperationResult(operation, result, requestId) {
  const opcode = operation[0];
  const primaryKey = operation[1];

  const expectedValue = (primaryKey == 1 || primaryKey == 3) ?
      largeValue(wrapThreshold, primaryKey) : [`small-${primaryKey}`];

  const requestIndex = result[0];
  assert_equals(
      requestIndex, requestId, 'result event order should match request order');
  switch (opcode) {
    case 'put':
    case 'put-with-id':
    case 'add':
      assert_equals(
          result[1], primaryKey,
          `${opcode} result should be the new object's primary key`);
      break;
    case 'get':
      assert_equals(
          result[1].id, primaryKey,
          'get result should match put value (primary key)');
      assert_equals(
          result[1].key, `k${primaryKey}`,
          'get result should match put value (key)');
      assert_equals(
          result[1].value.join(','), expectedValue.join(','),
          'get result should match put value (nested value)');
      break;
    case 'getall':
      assert_equals(
          result[1].length, primaryKey,
          'getAll should return all the objects in the store');
      for (let i = 0; i < primaryKey; ++i) {
        const object = result[1][i];
        assert_equals(
            object.id, i + 1,
            `getAll result ${i + 1} should match put value (primary key)`);
        assert_equals(
            object.key, `k${i + 1}`,
            `get result ${i + 1} should match put value (key)`);

        const expectedValue = (i == 0 || i == 2) ?
            largeValue(wrapThreshold, i + 1) : [`small-${i + 1}`];
        assert_equals(
            object.value.join(','), object.value.join(','),
            `get result ${i + 1} should match put value (nested value)`);
      }
      break;
    case 'get-empty':
      assert_equals(
          result[1], undefined, 'get-empty result should be undefined');
      break;
    case 'error':
      assert_equals(
          result[1].name, 'ConstraintError',
          'incorrect error from put with duplicate primary key');
      break;
    case 'continue':
    case 'open':
      assert_equals(
          result[1], `k${primaryKey}`,
          `${opcode} key should match the key in the put value`);
      assert_equals(
          result[2], primaryKey,
          `${opcode} primary key should match the put value's primary key`);
      assert_equals(
          result[3].id, primaryKey,
          `${opcode} value should match put value (primary key)`);
      assert_equals(
          result[3].key, `k${primaryKey}`,
          `${opcode} value should match put value (key)`);
      assert_equals(
          result[3].value.join(','), expectedValue.join(','),
          `${opcode} value should match put value (nested value)`);
      break;
    case 'continue-empty':
    case 'open-empty':
      assert_equals(result[1], null, `${opcode} result should be null`);
      break;
  }
}

function eventsTest(label, operations) {
  promise_test(testCase => {
    return createDatabase(testCase, (database, transaction) => {
      const store = database.createObjectStore(
          'test-store', { autoIncrement: true, keyPath: 'id' });
      store.createIndex('test-index', 'key', { unique: true });
      populateStore(store);
    }).then(database => {
      const transaction = database.transaction(['test-store'], 'readwrite');
      const store = transaction.objectStore('test-store');
      const index = store.index('test-index');
      return new Promise((resolve, reject) => {
        openCursors(testCase, index, operations, reject, () => {
          const results = [];
          const promises = [];
          for (let i = 0; i < operations.length; ++i) {
            const promise = doOperation(
                testCase, store, index, operations[i], i, results);
            promises.push(promise);
          };
          resolve(Promise.all(promises).then(() => results));
        });
      });
    }).then(results => {
      assert_equals(
          results.length, operations.length,
          'Promise.all should resolve after all sub-promises resolve');
      for (let i = 0; i < operations.length; ++i)
        checkOperationResult(operations[i], results[i], i);
    });
  }, label);
}

eventsTest('small values', [
  ['get', 2],
  ['count', 4],
  ['continue-empty', null],
  ['get-empty', 5],
  ['add', 5],
  ['open', 2],
  ['continue', 2],
  ['get', 4],
  ['get-empty', 6],
  ['count', 5],
  ['put-with-id', 5],
  ['put', 6],
  ['error', 3],
  ['continue', 4],
  ['count', 6],
  ['get-empty', 7],
  ['open', 4],
  ['open-empty', 7],
  ['add', 7],
]);

eventsTest('large values', [
  ['open', 1],
  ['get', 1],
  ['getall', 4],
  ['get', 3],
  ['continue', 3],
  ['open', 3],
]);

eventsTest('large value followed by small values', [
  ['get', 1],
  ['getall', 4],
  ['open', 2],
  ['continue-empty', null],
  ['get', 2],
  ['get-empty', 5],
  ['count', 4],
  ['continue-empty', null],
  ['open-empty', 5],
  ['add', 5],
  ['error', 1],
  ['continue', 2],
  ['get-empty', 6],
  ['put-with-id', 5],
  ['put', 6],
]);

eventsTest('large values mixed with small values', [
  ['get', 1],
  ['get', 2],
  ['get-empty', 5],
  ['count', 4],
  ['continue-empty', null],
  ['open', 1],
  ['continue', 2],
  ['open-empty', 5],
  ['getall', 4],
  ['open', 2],
  ['continue-empty', null],
  ['add', 5],
  ['get', 3],
  ['count', 5],
  ['get-empty', 6],
  ['put-with-id', 5],
  ['getall', 5],
  ['continue', 3],
  ['open-empty', 6],
  ['put', 6],
  ['error', 1],
  ['continue', 2],
  ['open', 4],
  ['get-empty', 7],
  ['count', 6],
  ['continue', 3],
  ['add', 7],
  ['getall', 7],
  ['error', 3],
  ['count', 7],
]);

</script>
